SPDX-FileCopyrightText: 2026 Richard Penaud & Gijs Netto SPDX-FileCopyrightText: 2026 AlICe laboratory https://alicelab.be
SPDX-License-Identifier: GPL-3.0-or-later
import math
import random
import bpydef clean_collection(collection_name): if collection_name in bpy.data.collections:
col = bpy.data.collections[collection_name] for obj in list(col.objects):
bpy.data.objects.remove(obj, do_unlink=True) bpy.ops.outliner.orphans_purge(
do_local_ids=True,
do_linked_ids=True,
do_recursive=True,
)— APPEL DE LA FONCTION —
clean_collection("modular")FONCTIONS###################################################################################
def box(_pos, _size):
bpy.ops.mesh.primitive_cube_add(size=_size, location=_pos)
return bpy.context.active_objectModular et Violon#####################################################################”
def convert_to_mesh(_obj):
bpy.ops.object.select_all(action="DESELECT")
_obj.select_set(True)
bpy.context.view_layer.objects.active = _obj
bpy.ops.object.convert(target="MESH")
return bpy.context.active_objectdef add_metaball(_mball_obj, _pos, _radius):
element = _mball_obj.data.elements.new()
element.co = _pos
element.radius = _radiusdef boolean_intersect(operandA, operandB):
mod = operandA.modifiers.new(name="_boolean", type="BOOLEAN")
mod.operation = "INTERSECT"
mod.object = operandB
mod.solver = "EXACT"
bpy.ops.object.select_all(action="DESELECT")
operandA.select_set(True)
bpy.context.view_layer.objects.active = operandA
bpy.ops.object.modifier_apply(modifier=mod.name)
bpy.data.objects.remove(operandB, do_unlink=True)def boolean_difference(operandA, operandB):
mod = operandA.modifiers.new(name="_boolean", type="BOOLEAN")
mod.operation = "DIFFERENCE"
mod.object = operandB
mod.solver = "EXACT"
bpy.ops.object.select_all(action="DESELECT")
operandA.select_set(True)
bpy.context.view_layer.objects.active = operandA
bpy.ops.object.modifier_apply(modifier=mod.name)
bpy.data.objects.remove(operandB, do_unlink=True)def boolean_union(operandA, operandB):
mod = operandA.modifiers.new(name="_boolean", type="BOOLEAN")
mod.operation = "UNION"
mod.object = operandB
mod.solver = "EXACT"
bpy.ops.object.select_all(action="DESELECT")
operandA.select_set(True)
bpy.context.view_layer.objects.active = operandA
bpy.ops.object.modifier_apply(modifier=mod.name)
bpy.data.objects.remove(operandB, do_unlink=True)2D################################################################################
def _text(text_list, location=(0, 0, 0), size=1.0): full_text = "\n".join(text_list) bpy.ops.object.text_add(location=location)
text_obj = bpy.context.active_object text_obj.data.body = full_text
text_obj.data.size = size # Règle la taille de la police text_obj.data.align_x = "LEFT"
return text_objdef bezier_rectangle(name, width, height, location=(0, 0, 0)): curve_data = bpy.data.curves.new(name=name, type="CURVE")
curve_data.dimensions = "3D"Créer une spline (une ligne de courbe) dans cette structure
spline = curve_data.splines.new(type="BEZIER")
spline.use_cyclic_u = True # Ferme la courbe (relie le dernier point au premier)Par défaut, une nouvelle spline a déjà 1 point. On en ajoute 3 autres pour faire 4 coins
spline.bezier_points.add(3) w = width / 2
h = height / 2Coordonnées des 4 coins (X, Y, Z)
coords = [
(-w, -h, 0),
(w, -h, 0),
(w, h, 0),
(-w, h, 0),
] for i, pt in enumerate(spline.bezier_points):
pt.co = coords[i]
pt.handle_left_type = "VECTOR"
pt.handle_right_type = "VECTOR"
obj = bpy.data.objects.new(name, curve_data)
obj.location = locationLier
bpy.context.collection.objects.link(obj)
return objtransition - grid#######################################################################
def generateGrid(_GridName, size_val, location_tuple):
bpy.ops.mesh.primitive_grid_add(
x_subdivisions=45,
y_subdivisions=45,
location=location_tuple,
size=size_val,
)
bpy.context.object.name = _GridNamedef Remodel(_Mesh, size_val):
myObj = bpy.data.objects[_Mesh]
num_bumps = random.randint(5, 8)
bumps = []
half_size = size_val / 2.0
for _ in range(num_bumps):
bumps.append(
{
"center_x": random.uniform(-half_size * 1.1, half_size * 1.1),
"center_y": random.uniform(-half_size * 1.1, half_size * 1.1),
"radius": random.uniform(half_size * 0.6, half_size * 1.2),
"amplitude": random.uniform(-2.0, 2.0),
},
)
for v in myObj.data.vertices:
ratio_x = (v.co.x + half_size) / size_val
vague_sinus = math.sin((ratio_x * math.pi) - (math.pi / 2.0))
v.co.z = (size_val * 0.25) + ((size_val * 0.35) * vague_sinus)
for b in bumps:
dist = math.sqrt(
(v.co.x - b["center_x"]) ** 2 + (v.co.y - b["center_y"]) ** 2
)
if dist < b["radius"]:
influence = (math.cos((dist / b["radius"]) * math.pi) + 1.0) / 2.0
v.co.z += b["amplitude"] * influencedef stretch_it(_Mesh, size_val):
obj = bpy.data.objects[_Mesh]
half_size = size_val / 2.0
margin = size_val * 0.012
for v in obj.data.vertices:
if (half_size - margin) < v.co.x < (half_size + margin):
v.co.x = half_size * 1.4
if (-half_size + margin) > v.co.x > (-half_size - margin):
v.co.x = -half_size * 1.4
if (half_size - margin) < v.co.y < (half_size + margin):
v.co.y = half_size * 1.2
if (-half_size + margin) > v.co.y > (-half_size - margin):
v.co.y = -half_size * 1.2def _Modifiers(_Mesh, _Thick):
myObj = bpy.data.objects[_Mesh]
smooth_it = myObj.modifiers.new(name="smooth_it", type="SUBSURF")
smooth_it.levels = 4
extrusion = myObj.modifiers.new(name="extrusion", type="SOLIDIFY")
extrusion.thickness = _ThickEXE########################################################################################
bpy.ops.object.camera_add(
enter_editmode=False,
align="VIEW",
location=(10, 10, 10),
rotation=(0, 0, 0),
)
bpy.context.object.data.type = "ORTHO"
bpy.context.object.data.ortho_scale = 32
bpy.ops.object.camera_add(
enter_editmode=False,
align="VIEW",
location=(10, -10, 15),
rotation=(math.pi / 4, 0, math.pi / 4),
)
bpy.context.object.data.type = "ORTHO"
bpy.context.scene.render.resolution_x = 1080
bpy.context.object.data.ortho_scale = 32ou dans le morceau
_partie = random.randint(1, 5)2D
bpy.ops.curve.primitive_nurbs_circle_add(location=(-3, 22, 0))
bpy.ops.transform.resize(value=(2, 1, 1))
bpy.ops.curve.primitive_nurbs_circle_add(location=(4, 22, 0))
bpy.ops.transform.resize(value=(5, 1, 1))
bpy.ops.curve.primitive_nurbs_circle_add(location=(12, 22, 0))
bpy.ops.transform.resize(value=(3, 1, 1))
bpy.ops.curve.primitive_nurbs_circle_add(location=(20, 22, 0))
bpy.ops.transform.resize(value=(5, 1, 1))
if _partie == 1:
bpy.ops.object.text_add(location=(-5, 18, 0), scale=(1, 1, 1))
text_obj = bpy.context.active_object
text_obj.data.body = "Partie I: intro"
bezier_rectangle(
name="RectangleBezier", width=4.0, height=4.0, location=(-4, 22, 0)
)
l_texte = [
"0'00-0'40",
"génération de la grille des bass, drums et cords.",
"génération du violon.",
]
_text(l_texte, location=(-5, 17, 0), size=0.5)
if _partie == 2:
bpy.ops.object.text_add(location=(-5, 18, 0), scale=(1, 1, 1))
text_obj = bpy.context.active_object
text_obj.data.body = "Transition entre intro et partie 2"
bezier_rectangle(
name="RectangleBezier", width=3.0, height=4.0, location=(-1, 22, 0)
)
l_texte = [
"0'40-1'00",
"génération de la grille des bass, drums et cords.",
"génération du violon et du modular.",
"génération de la vague de transition.",
]
_text(l_texte, location=(-5, 17, 0), size=0.5)
if _partie == 3:
bpy.ops.object.text_add(location=(-5, 18, 0), scale=(1, 1, 1))
text_obj = bpy.context.active_object
text_obj.data.body = "Partie 2"
bezier_rectangle(
name="RectangleBezier", width=12.0, height=4.0, location=(7, 22, 0)
)
l_texte = [
"1'00-3'50",
"génération de la grille des bass, drums et cords.",
"génération du modular.",
]
_text(l_texte, location=(-5, 17, 0), size=0.5)
if _partie == 4:
bpy.ops.object.text_add(location=(-5, 18, 0), scale=(1, 1, 1))
text_obj = bpy.context.active_object
text_obj.data.body = "Transition entre la partie 2 et la partie finale"
bezier_rectangle(
name="RectangleBezier", width=4.0, height=4.0, location=(15, 22, 0)
)
l_texte = [
"3'50-4'20",
"génération de la grille des bass, drums et cords.",
"génération du modular et du violon.",
"génération de la vague de transition.",
]
_text(l_texte, location=(-5, 17, 0), size=0.5)
if _partie == 5:
bpy.ops.object.text_add(location=(-5, 18, 0), scale=(1, 1, 1))
text_obj = bpy.context.active_object
text_obj.data.body = "Partie finale"
bezier_rectangle(
name="RectangleBezier", width=7.0, height=4.0, location=(20, 22, 0)
)
l_texte = [
"4'20-5'59",
"génération de la grille des bass, drums et cords.",
"génération du violon.",
]
_text(l_texte, location=(-5, 17, 0), size=0.5)Settings
c_ = 10 # Taille de la scène
d_ = 5 # zone des mball debut
num_layers = 5 # Nombre de couches verticales
balls_per_layer = 5 # Nombre de metaballs par couche
layer_height = 2.0 # Hauteur entre chaque couche
num_lines = 3
balls_per_line = 3
shell_thickness = 0.3STRUCTURE###################################################################################### grille Bass / Drums / Cords
if "modular" in bpy.data.collections:
col = bpy.data.collections["modular"]
else:
col = bpy.data.collections.new("modular")
bpy.context.scene.collection.children.link(col)
all_rods = []
YZ_rods = []
XZ_rods = []
drums = [-4.75, -2.5, 0.5, 3.5, 4.75] # trio serré + espacées + trio serré
cords = [-4.85, -3.5, -1, 2, 4.85] # 2 proches + 3 espacées
bass = [-4.6, -2.5, 2, 3.3, 4.6] # duo + vide + trio serré
w_drums = 0.5
w_cords = 0.3
w_bass = 0.8
for ox in drums:
for oy in cords:tige verticale Z
bpy.ops.mesh.primitive_cube_add(
size=1,
location=(ox, oy, 0),
scale=(w_drums, w_cords, c_),
)
rod = bpy.context.active_object
all_rods.append(rod)
base = all_rods[0]
for rod in all_rods[1:]:
boolean_union(base, rod)
copieYZ = base.copy()
copieYZ.data = base.data.copy()
copieYZ.name = "copieYZ"
bpy.context.collection.objects.link(copieYZ)
copieXZ = base.copy()
copieXZ.data = base.data.copy()
copieXZ.name = "copieXZ"
bpy.context.collection.objects.link(copieXZ)
for oy in cords:
for oz in bass:tige horizontale X
bpy.ops.mesh.primitive_cube_add(
size=1,
location=(0, oy, oz),
scale=(c_, w_cords, w_bass),
)
rod = bpy.context.active_object
YZ_rods.append(rod)
baseYZ = YZ_rods[0]
for rod in YZ_rods[1:]:
boolean_union(baseYZ, rod)
boolean_difference(baseYZ, copieYZ)
for ox in drums:
for oz in bass:tige horizontale Y (fine)
bpy.ops.mesh.primitive_cube_add(
size=1,
location=(ox, 0, oz),
scale=(w_drums, c_, w_bass),
)
rod = bpy.context.active_object
XZ_rods.append(rod)
baseXZ = XZ_rods[0]
for rod in XZ_rods[1:]:
boolean_union(baseXZ, rod)
boolean_difference(baseXZ, copieXZ)Fusionner toutes les tiges
boolean_union(base, baseYZ)
boolean_union(base, baseXZ)
base.name = "grid_final"
if _partie == 1 or _partie == 2 or _partie == 5:VIOLON###################################################################################### Étape 1 : Création des metaballs (ellipsoïdes)
bpy.ops.object.metaball_add(type="BALL", location=(0, 0, 0), radius=0.001)
mball_violon = bpy.context.active_object
mball_violon.name = "mball_violon"
mball_violon.data.resolution = 0.2
randnom = random.randint(2, 4)
for r in range(randnom):
num_balls = 32 # Nombre total de billes pour former la spirale
radius = d_ * random.randint(1, 3) / 4 # Rayon de la spirale (adapté à ton d_)
height = d_ * random.randint(1, 3) / 2 # Hauteur totale de la spirale
randangle = random.randint(-3, 3) / 2
count = 20
for i in range(num_balls):‘t’ varie de 0 à 1 le long de la spirale
t = i / (num_balls - 1)Un tour complet correspond à 2 * pi radians
angle = math.pi * t + randangleCalcul des coordonnées (Cercle en X,Y et montée en Z)
px = radius * math.cos(angle)
py = radius * math.sin(angle)
if randangle in range(-3, 3, 1):
pz = (t * height) - (height / 2) # Centré sur l'origine
else:
pz = (-t * height) + (height / 2) # Centré sur l'origineTaille et épaisseur comme dans ton code
_size = count / 30
pos = (px, py, pz)
add_metaball(mball_violon, pos, _size)
count += 1Étape 2 : Conversion en mesh
mesh_violon = convert_to_mesh(mball_violon)
mesh_violon.name = "mesh_violon"
if _partie == 2 or _partie == 3 or _partie == 4:MODULAR####################################################################################### étape 1 : metaballs
bpy.ops.object.metaball_add(type="BALL", location=(0, 0, 0), radius=0.001)
mball_outer = bpy.context.active_object
mball_outer.name = "mball_outer"
mball_outer.data.resolution = 0.2
bpy.ops.object.metaball_add(type="BALL", location=(0, 0, 0), radius=0.001)
mball_inner = bpy.context.active_object
mball_inner.name = "mball_inner"
mball_inner.data.resolution = 0.2
num_lines = 7
balls_per_line = 7
for line in range(num_lines):
px = random.uniform(-d_ / 2, d_ / 2)
py = random.uniform(-d_ / 2, d_ / 2)
pz = random.uniform(-d_ / 2, d_ / 2)
dx = random.uniform(-1, 1)
dy = random.uniform(-1, 1)
dz = random.uniform(-1, 1)
step = random.uniform(0.8, 1.4)
for i in range(balls_per_line):
_size = random.uniform(1, 4)
pos = (px + dx * i * step, py + dy * i * step, pz + dz * i * step)
add_metaball(mball_outer, pos, _size)
add_metaball(mball_inner, pos, _size - shell_thickness)étape 2 : conversion mesh
mesh_outer = convert_to_mesh(mball_outer)
mesh_outer.name = "mesh_outer"
mesh_inner = convert_to_mesh(mball_inner)
mesh_inner.name = "mesh_inner"Backup pour la grille
modular_backup = mesh_outer.copy()
modular_backup.data = mesh_outer.data.copy()
modular_backup.name = "modular_backup"
bpy.context.collection.objects.link(modular_backup)étape 3 : booleans metaballs
work_volume = box((0, 0, 0), c_)
boolean_intersect(work_volume, mesh_outer)
boolean_difference(work_volume, mesh_inner)
work_volume.name = "modular"
boolean_difference(base, modular_backup)
if _partie == 2 or _partie == 4:TRANSITION#####################################################################################
GENERATION DE MYGRID ALEATOIRE
random_size = random.uniform(4, 8) cube_bound = 12 / 2
rand_x = random.uniform(-cube_bound, cube_bound)
rand_y = random.uniform(-cube_bound, cube_bound)
rand_z = random.uniform(-cube_bound, cube_bound)
random_location = (rand_x, rand_y, rand_z)Production du maillage de base
generateGrid("myGrid", random_size, random_location)
Remodel("myGrid", random_size)
stretch_it("myGrid", random_size)
_Modifiers("myGrid", shell_thickness)Conversion en mesh destructif pour pouvoir appliquer les booléens correctement
mesh_outer = convert_to_mesh(bpy.data.objects["myGrid"])
mesh_outer.name = "transition"Appliquer le visuel final (lissage des normales)
bpy.ops.object.shade_smooth()DECOUPE PAR LE CUBE
Création du cadre du cube (volume de travail de dimension c_ = 10)
work_volume = box((0, 0, 0), c_)Intersection : On ne garde de myGrid QUE ce qui est à l’intérieur du cube de 10x10x10
boolean_intersect(mesh_outer, work_volume)Note : mesh_outer est maintenant le plan découpé net aux dimensions du cube.
fin!